package com.googlecode.mvcguice.routing;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;

/** 
 * This class represents URL pattern used in routing and provides facilities for it's validation and parsing.
 * 
 * Allowed path element chars described in <a href="http://www.ietf.org/rfc/rfc1738.txt">RFC</a>:
 * 
 * uchar          = unreserved | escape
 * 
 * unreserved     = alpha | digit | safe | extra
 * 
 * alpha          = lowalpha | hialpha
 * 
 * digit          = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
 * 
 * safe           = "$" | "-" | "_" | "." | "+"
 * 
 * extra          = "!" | "*" | "'" | "(" | ")" | ","
 * 
 * lowalpha       = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" |
 *                  "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" |
 *                  "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" |
 *                  "y" | "z"
 *                  
 * hialpha        = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" |
 *                  "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" |
 *                  "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
 * escape         = "%" hex hex
 * 
 * hex            = digit | "A" | "B" | "C" | "D" | "E" | "F" |
 *                          "a" | "b" | "c" | "d" | "e" | "f"
 */
//TODO: add ".html" support in pattern
public final class UrlPattern {
	private static final Logger LOGGER = LoggerFactory.getLogger(UrlPattern.class);

	private static final String STATIC_SEGMENT_PATTERN = "[(\\w-!,'\\$\\.\\+\\*\\(\\)]+";
	private static final String DYNAMIC_SEGMENT_PATTERN = "\\{(\\w+)\\}";
	private static final Pattern URL_PATTERN_MATCHER =
			Pattern.compile("/|(/(" + DYNAMIC_SEGMENT_PATTERN + "|" + STATIC_SEGMENT_PATTERN + "))+/?");
	private static final Pattern DYNAMIC_SEGMENT_MATCHER = Pattern.compile(DYNAMIC_SEGMENT_PATTERN);

	private static final String URL_DYNAMIC_SEGMENT_PATTERN = "([^/]+)";

	private final String urlPattern;
	private final Pattern urlMatcher;
	private final List<String> variables;

	public UrlPattern(String urlPattern) {
		Preconditions.checkArgument(isValid(urlPattern), "Invalid URL pattern");
		this.urlPattern = urlPattern;

		final Matcher dynSegMatcher = DYNAMIC_SEGMENT_MATCHER.matcher(urlPattern);
		variables = extractVariables(dynSegMatcher);
		urlMatcher = urlMatcher(dynSegMatcher);

		LOGGER.debug("For URL pattern: '{}' extracted variables: {} and compiled URL matcher regex: '{}'",
				new Object[] { urlPattern, variables, urlPattern });
	}

	public List<String> getVariables() {
		return variables;
	}

	public Map<String, String> match(String url) {
		final Matcher matcher = urlMatcher.matcher(url);
		if (!matcher.matches()) {
			return null;
		}
		if (variables.isEmpty()) {
			return Collections.emptyMap();
		}

		final Map<String, String> result = new HashMap<String, String>(variables.size());
		for (int i = 0, n = variables.size(); i < n; i++) {
			result.put(variables.get(i), matcher.group(i + 1));
		}
		return result;
	}

	public String createUrl(Map<String, String> variables) {
		return buildUrl(urlPattern, variables);
	}

	@Override
	public String toString() {
		return "(" + urlMatcher + ")=>" + variables;
	}

	public static boolean isValid(String urlPattern) {
		return URL_PATTERN_MATCHER.matcher(urlPattern).matches();
	}

	private static String buildUrl(String pattern, Map<String, String> variables) {
		final StringBuilder sb = new StringBuilder(pattern.length() * 2);

		final int n = pattern.length();
		for (int i = 0; i < n;) {
			char c = pattern.charAt(i);
			i++;
			if (c == '{') {
				final StringBuilder vb = new StringBuilder();
				/* 
				 * We are safe here since pattern is validated before and we don't expect incomplete vars.
				 */
				for (;;) {
					c = pattern.charAt(i);
					i++;
					if (c == '}') {
						break;
					} else {
						vb.append(c);
					}
				}
				final String var = vb.toString();
				final String val = variables.get(var);
				if (val == null) {
					// TODO: investigate it
					throw new RuntimeException("For var: '" + var + "' value is null");
				}
				sb.append(val);
			} else {
				sb.append(c);
			}
		}

		return sb.toString();
	}

	private static final String LITERAL_BEGIN = "\\Q";
	private static final String LITERAL_END = "\\E";

	private static Pattern urlMatcher(Matcher matcher) {
		matcher.reset();

		String replacement = "\\" + LITERAL_END + URL_DYNAMIC_SEGMENT_PATTERN + "\\" + LITERAL_BEGIN;
		StringBuffer sb = new StringBuffer();
		sb.append(LITERAL_BEGIN);
		while (matcher.find()) {
			matcher.appendReplacement(sb, replacement);
		}
		matcher.appendTail(sb);
		sb.append(LITERAL_END);

		String pattern = sb.toString();
		return Pattern.compile(pattern);
	}

	private static List<String> extractVariables(Matcher matcher) {
		ArrayList<String> variables = new ArrayList<String>();

		matcher.reset();
		while (matcher.find()) {
			variables.add(matcher.group(1));
		}

		variables.trimToSize();
		return Collections.unmodifiableList(variables);
	}
}
